昨天我們完成了 Roman Numeral Kata,現在進入框架特定測試的新階段!想像一個場景:專案上線前夕,PM 緊張地問:「API 都測試過了嗎?」你自信地回答:「每個端點都有完整的測試覆蓋!」這就是今天要學習的 HTTP 測試。
基礎階段             Kata 階段            框架特定測試
Days 1-10           Days 11-17           Days 18-27
   ✅                  ✅                  📍 Day 18
                                        [HTTP 測試基礎] <- 今天在這
                                              ⬇️
                                        下一階段:更多測試技巧
在 Python 中,HTTP 測試允許我們:
| 面向 | 單元測試 | HTTP 測試 | 
|---|---|---|
| 測試範圍 | 單一函數或類別 | 完整請求流程 | 
| 執行速度 | 極快 | 快 | 
| 測試重點 | 邏輯正確性 | 整合正確性 | 
| 覆蓋範圍 | 細節實作 | 使用者體驗 | 
# 安裝 FastAPI 和測試工具
pip install fastapi httpx pytest-asyncio
# 建立 tests/day18/test_http_basics.py
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
app = FastAPI()
@app.get("/api/health")
def health_check():
    return {"status": "ok"}
client = TestClient(app)
def test_can_make_successful_get_request():
    response = client.get("/api/health")
    
    assert response.status_code == 200
    assert response.json() == {"status": "ok"}
# 建立 tests/day18/test_http_methods.py
from fastapi import FastAPI, HTTPException
from fastapi.testclient import TestClient
from pydantic import BaseModel
app = FastAPI()
class User(BaseModel):
    name: str
    email: str
users_db = {}
@app.get("/api/users")
def get_users():
    return list(users_db.values())
@app.post("/api/users", status_code=201)
def create_user(user: User):
    user_dict = user.model_dump()
    user_dict["id"] = len(users_db) + 1
    users_db[user_dict["id"]] = user_dict
    return user_dict
@app.delete("/api/users/{user_id}", status_code=204)
def delete_user(user_id: int):
    if user_id not in users_db:
        raise HTTPException(status_code=404, detail="User not found")
    del users_db[user_id]
client = TestClient(app)
def test_supports_different_http_methods():
    # POST 請求
    response = client.post("/api/users", json={
        "name": "John Doe",
        "email": "john@example.com"
    })
    assert response.status_code == 201
    user_id = response.json()["id"]
    
    # GET 請求
    response = client.get("/api/users")
    assert response.status_code == 200
    assert len(response.json()) == 1
    
    # DELETE 請求
    response = client.delete(f"/api/users/{user_id}")
    assert response.status_code == 204
# 建立 tests/day18/test_json_validation.py
def test_can_send_and_receive_json_data():
    response = client.post("/api/users", json={
        "name": "John Doe",
        "email": "john@example.com",
        "age": 30
    })
    
    assert response.status_code == 201
    data = response.json()
    
    # 驗證回應結構
    assert "id" in data
    assert data["name"] == "John Doe"
    assert data["email"] == "john@example.com"
    assert data["age"] == 30
    
    # 驗證資料類型
    assert isinstance(data["id"], int)
    assert isinstance(data["name"], str)
# 建立 tests/day18/test_task_api.py
import pytest
from fastapi import FastAPI, HTTPException
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field
app = FastAPI()
class TaskCreate(BaseModel):
    title: str = Field(..., min_length=1, max_length=255)
    completed: bool = False
tasks_db = {}
@app.get("/api/tasks")
def get_tasks():
    return {"data": list(tasks_db.values())}
@app.post("/api/tasks", status_code=201)
def create_task(task: TaskCreate):
    task_id = len(tasks_db) + 1
    new_task = {
        "id": task_id,
        "title": task.title,
        "completed": task.completed
    }
    tasks_db[task_id] = new_task
    return {"data": new_task}
@app.get("/api/tasks/{task_id}")
def get_task(task_id: int):
    if task_id not in tasks_db:
        raise HTTPException(status_code=404, detail="Task not found")
    return {"data": tasks_db[task_id]}
@app.delete("/api/tasks/{task_id}", status_code=204)
def delete_task(task_id: int):
    if task_id not in tasks_db:
        raise HTTPException(status_code=404, detail="Task not found")
    del tasks_db[task_id]
client = TestClient(app)
@pytest.fixture(autouse=True)
def setup():
    tasks_db.clear()
    yield
    tasks_db.clear()
def test_can_create_and_get_task():
    # 建立任務
    response = client.post("/api/tasks", json={
        "title": "Learn HTTP Testing",
        "completed": False
    })
    assert response.status_code == 201
    task_id = response.json()["data"]["id"]
    
    # 取得任務
    response = client.get(f"/api/tasks/{task_id}")
    assert response.status_code == 200
    assert response.json()["data"]["title"] == "Learn HTTP Testing"
def test_validates_task_creation():
    # 缺少必要欄位
    response = client.post("/api/tasks", json={})
    assert response.status_code == 422
    
    # 標題為空
    response = client.post("/api/tasks", json={"title": ""})
    assert response.status_code == 422
def test_handles_not_found():
    response = client.get("/api/tasks/999")
    assert response.status_code == 404
    assert response.json()["detail"] == "Task not found"
def test_can_delete_task():
    # 建立任務
    create_response = client.post("/api/tasks", json={
        "title": "Task to Delete"
    })
    task_id = create_response.json()["data"]["id"]
    
    # 刪除任務
    response = client.delete(f"/api/tasks/{task_id}")
    assert response.status_code == 204
    
    # 確認已刪除
    get_response = client.get(f"/api/tasks/{task_id}")
    assert get_response.status_code == 404
# 建立 tests/day18/test_best_practices.py
import pytest
def test_handles_invalid_json():
    response = client.post(
        "/api/users",
        data="{invalid json}",
        headers={"Content-Type": "application/json"}
    )
    assert response.status_code == 422
def test_handles_missing_fields():
    response = client.post("/api/users", json={"email": "test@example.com"})
    assert response.status_code == 422
@pytest.fixture(autouse=True)
def reset_data():
    """確保每個測試都有乾淨的環境"""
    users_db.clear()
    yield
    users_db.clear()
def test_data_isolation():
    # 每個測試都應該有乾淨的狀態
    assert len(users_db) == 0
試著實作這些功能的測試:
今天我們學習了 Python FastAPI HTTP 測試的基礎:
HTTP 測試是 API 開發的基石,讓我們能在不啟動伺服器的情況下,完整測試 API 的行為。
明天我們將學習更多框架特定的測試技巧,包括如何測試更複雜的應用場景。準備好繼續深入探索了嗎?明天見! 🚀